今天我們將在帳務報表頁面實作一個圓餅圖,使用 DGCharts 顯示每個物品的分類比例,讓我們能更方便地了解各分類在整體帳務中的比例。除了圓餅圖之外,還會顯示物品清單和總金額,提升報表功能的完整性。
今天的實作目標主要是實現以下幾個功能:
DGCharts 是一個功能強大的圖表庫,專為 iOS 和 macOS 平台開發,基於 MPAndroidChart。它支援多種圖表類型,包括折線圖、柱狀圖、圓餅圖等,並且提供了豐富的自定義選項,讓開發者可以輕鬆地創建專業的資料視覺化圖表。在 SwiftUI 中,通過 UIViewRepresentable,我們可以將 DGCharts 與 SwiftUI 結合,從而實現更靈活的數據展示。DGCharts 是一個開源專案,在使用上具有極高的可擴展性和靈活性,非常適合用來處理動態資料和視覺化分析。
參考資料:
我們需要一個 ReportViewModel,它負責處理所有的資料抓取邏輯。
ReportViewModel 在初始化時會自動從資料庫中抓取資料,並且儲存到 items 中。
import DGCharts
import SwiftUI
class ReportViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var dataEntries: [PieChartDataEntry] = []
@Published var chartColors: [UIColor] = []
let dataManager: DataManager
init(dataManager: DataManager = DataManager()) {
self.dataManager = dataManager
fetchData()
}
}
我們需要在 fetchData() 函數中撈取物品資料,並根據每個物品的分類進行統計。
func fetchData() {
items = dataManager.fetchItems()
generatePieChartData(items: items)
}
使用 dataManager.fetchItems() 來抓取所有的物品資料。接著,將這些資料傳遞給 generatePieChartData(),生成圓餅圖的資料。
接下來,我們需要根據每個物品的分類來生成對應的圓餅圖資料,並且給予每筆資料對應的顏色。
func generatePieChartData(items: [Item]) {
if items.count > 0 {
var categoryAmounts: [ItemCategory: Double] = [:]
for item in items {
let category = item.category
categoryAmounts[category, default: 0] += item.price
}
dataEntries = categoryAmounts.map { (category, amount) in
PieChartDataEntry(value: amount, label: category.name)
}
chartColors = categoryAmounts.map { category, _ in
UIColor(hexString: category.categoryGroup.colorHex)
}
} else {
dataEntries = [PieChartDataEntry(value: 1)]
chartColors = [UIColor(cgColor: CGColor(red: 80 / 255, green: 80 / 255, blue: 80 / 255, alpha: 1))]
}
}
先用 categoryAmounts 來統計每個分類的金額,接著利用 PieChartDataEntry 生成對應的圓餅圖資料。最後,為每個分類設定一個顏色,而這個顏色來自於物品的大分類設定。
ReportPieChartView 是一個使用 UIViewRepresentable 的自定義 View,專門用來在 SwiftUI 中顯示 DGCharts 的圓餅圖。我們將使用 @Binding 將資料傳遞進來,當資料更新時,圓餅圖也能自動刷新。
我們先將 ReportPieChartView 初始化。在 makeUIView 中,我們設定了圓餅圖的基本樣式,包括是否顯示圖例、中心圓孔的大小,以及是否顯示資料的標籤。
import SwiftUI
import DGCharts
struct ReportPieChartView: UIViewRepresentable {
@Binding var items: [Item] // 綁定物品列表
var entries: [PieChartDataEntry] // 圓餅圖條目
var colors: [UIColor] // 顏色列表
func makeUIView(context: Context) -> PieChartView {
let chart = PieChartView()
chart.holeRadiusPercent = 0.5 // 中心圓孔的大小
chart.legend.enabled = false // 隱藏圖例
chart.centerText = "總金額" // 圓心顯示的文字
chart.drawEntryLabelsEnabled = false // 隱藏資料標籤
return chart
}
}
接下來要實作 updateUIView,這個函數會在資料變更時被呼叫。它會將我們從 ViewModel 中取得的資料和顏色更新到圓餅圖中。
func updateUIView(_ uiView: PieChartView, context: Context) {
let dataSet = PieChartDataSet(entries: entries, label: "") // 生成圖表資料集
dataSet.colors = colors // 設定資料的顏色
dataSet.selectionShift = 0 // // 點選後突出位置
dataSet.drawValuesEnabled = false // 不顯示資料的值
let data = PieChartData(dataSet: dataSet)
uiView.data = data
// 更新中心的文字顯示
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let totalAmount = entries.reduce(0, { $0 + $1.value }) // 計算總金額
let text = NSAttributedString(string: "總金額\n\(String(format: "$%.0f", totalAmount))", attributes: [
.font: UIFont.boldSystemFont(ofSize: 30),
.foregroundColor: UIColor.black,
.paragraphStyle: paragraphStyle
])
uiView.centerAttributedText = text
}
這段程式碼中,我們生成一個 PieChartDataSet,用來存放所有的資料,並設定每筆資料的顏色、字體等。接著,我們計算總金額,並將總金額顯示在圓餅圖的中心。
我們還需要處理一些細節來讓顯示效果更好,像是圓心文字的顯示,如果沒有物品,圓餅圖應顯示灰色並標示總金額為 0。
let text = NSAttributedString(string: "總金額\n\(String(format: "$%.0f", items.count > 0 ? totalAmount : 0))", attributes: [
.font: UIFont.boldSystemFont(ofSize: 30),
.foregroundColor: UIColor.black,
.paragraphStyle: paragraphStyle
])
這樣即使沒有資料,也能顯示灰色的圓餅圖,並且在圓心顯示「總金額 0」。
最後我們來建立 ReportView,它會包含圓餅圖和物品列表,並透過 ReportViewModel 撈取資料。接著在畫面中呼叫我們剛剛寫好的 ReportPieChartView ,並在圓餅圖的下方顯示家用品列表,若資料庫沒有家用品紀錄,則顯示沒有紀錄。
import SwiftUI
struct ReportView: View {
@ObservedObject var viewModel: ReportViewModel
init(viewModel: ReportViewModel = ReportViewModel()) {
self.viewModel = viewModel
}
var body: some View {
VStack {
ReportPieChartView(items: $viewModel.items, entries: viewModel.dataEntries, colors: viewModel.chartColors)
.padding(10)
.frame(height: 320)
Spacer()
if viewModel.items.count > 0 {
List(viewModel.items) { item in
HStack {
Text(item.name)
Spacer()
Text("\(item.price, specifier: "%.2f")")
}
}
} else {
VStack {
Image(systemName: "list.clipboard")
.resizable()
.frame(width: 100, height: 140)
.padding()
Text("沒有紀錄")
}
.foregroundColor(.gray)
Spacer()
}
}
.onAppear {
viewModel.fetchData()
}
.navigationTitle("帳務報表")
}
}
#Preview {
ReportView(viewModel: ReportViewModel())
}
今天我們成功將 DGCharts 整合到 SwiftUI 的 ReportView 中,並實現了圓餅圖的顯示。圓餅圖會根據物品的分類金額比例自動生成,並且中心顯示總金額。此外,我們也在圖表下方顯示了物品清單。今天先做到這邊,我們明天見!